Terraformで構築する機械学習ワークロード(Batch on Fargate編)
こんちには。
データアナリティクス事業本部 インテグレーション部 機械学習チームの中村です。
今回も「Terraformで構築する機械学習ワークロード」ということで、前回の記事ではLambdaを使いましたが、今回はその処理をBatch on Fargateに載せてみたいと思います。
前回記事は以下です。
構成イメージ
構成としては以下のようなものを作成していきます。
前回との違いとしては、まずLambdaの代わりにBatch on Fargateを使う点です。
Fargateのタスク(ジョブ)上のコンテナイメージで物体検出モデルの一つであるYOLOXを動かしていきます。
また、それ以外にもBatchを使用する場合は、S3イベントとBatchの間にEventBridgeが必要となります。
動作環境
Docker、Terraformはインストール済みとします。
Terraformを実行する際のAWSリソースへの権限は、aws-vaultで環境構築をしておきます。
aws-vaultについては以下も参考にされてください。
ホストPCとしてのバージョン情報配下です。
- OS: Windows 10 バージョン22H2
- docker: Docker version 24.0.2-rd, build e63f5fa
- terraform: Terraform v1.4.6 (on windows_amd64)
またコンテナ内のバージョン情報配下となります。
- Python: 3.10.12 (main, Aug 15 2023, 15:43:05) [GCC 7.3.1 20180712 (Red Hat 7.3.1-15)]
- PyTorch: 2.0.1+cu117
- OpenCV: 4.8.0
- MMEngine: 0.8.4
- MMDetection: 3.1.0+
成果物
成果物はGitHub上にあげておきましたので、詳細は必要に応じてこちらを参照ください。
フォルダ構成は以下のようになっています。
├─asset │ yolox_l_8x8_300e_coco_20211126_140236-d3bd2b23.pth │ ├─docker │ └─batch_fargate │ push_ecr.sh │ ├─python │ Dockerfile │ mmdetect_handler.py │ run.py │ s3_handler.py │ └─terraform ├─environments │ └─dev │ locals.tf │ main.tf │ variables.tf │ └─modules ├─batch │ main.tf │ outputs.tf │ variables.tf │ ├─ecr │ main.tf │ variables.tf │ ├─event_bridge │ main.tf │ variables.tf │ ├─iam │ main.tf │ outputs.tf │ variables.tf │ ├─s3 │ main.tf │ variables.tf │ └─vpc main.tf outputs.tf variables.tf
大筋のモデル構成は以下で前回と同じです。
asset/
: モデル置き場として使用docker/
: コンテナイメージのビルド時やECRへのpush時に使用Dockerfile
自体はpython
フォルダに配置
python/
:Dockerfile
に加えて、Pythonスクリプトを配置terraform/environments/dev/
: terraform実行時の環境に応じたメインterraform/modules/
: moduleに分割したtfファイルが配置
コードの説明
ここからはコードを説明します。Lambdaの時と違う点をメインに説明していきます。
コンテナイメージのビルド
まず以下のような内容でpython/Dockerfile
を準備しておきます。
FROM public.ecr.aws/lambda/python:3.10 WORKDIR /opt RUN pip3 install torch RUN pip install openmim && \ mim install "mmengine>=0.7.1" "mmcv>=2.0.0rc4" RUN yum install -y git RUN git clone https://github.com/open-mmlab/mmdetection.git WORKDIR /opt/mmdetection RUN pip3 install --no-cache-dir -e . RUN yum install -y tar RUN yum install -y mesa-libGL.x86_64 RUN mkdir -p /opt/mmdetection/checkpoints RUN mim download mmdet --config yolox_l_8x8_300e_coco --dest /opt/mmdetection/checkpoints RUN pip3 install boto3 COPY *.py /opt/mmdetection ENTRYPOINT [""] CMD ["echo", "hello world"]
ほぼ前回の記事と同じ内容です。
注意点としてはENTRYPOINT
を空白にすることで、Lambdaが使用するlambda-entrypoint.sh
を使用しないようにしています。
これによってdockerの起動時に任意のコマンドが実行できるようにしています。
ENTRYPOINT
を上書きしない場合、以下のエラーがログに表示され、うまく実行できないケースがありました。
entrypoint requires the handler name to be the first argument
その他Lambda環境の時と異なり、boto3
を明示的に入れる必要がありましたのでpip install
で追加しています。
前回記事のものをビルド済みの場合は、そちらをベースイメージとすれば高速にビルドできます。
Pythonファイルについて
Pythonファイルは以下の3種類あります。
│ mmdetect_handler.py │ run.py │ s3_handler.py
mmdetec_handler.py
とs3_handler.py
については前回と同じため割愛します。
run.py
run.py
は以下のような内容となります。
import os from mmdetect_handler import collect_env, find_checkpoint, inference from s3_handler import download_directory, download_file, upload_file def main(input_bucket_name: str, input_object_key: str): bucket_name = os.getenv("BUCKET_NAME") input_prefix = os.getenv("OBJECT_INPUT_PREFIX") output_prefix = os.getenv("OBJECT_OUTPUT_PREFIX") print(f"{bucket_name=}") print(f"{input_prefix=}") print(f"{output_prefix=}") # 処理対象のオブジェクトを取得 download_file("./input.jpg", input_bucket_name, input_object_key) # モデル等をダウンロード download_directory(destination_path="./checkpoints", bucket_name=bucket_name, prefix="asset/") # 環境ログを出力 for name, val in collect_env().items(): print(f"{name}: {val}") # モデルファイルを探索 checkpoint_file = find_checkpoint(model_name="yolox_l_8x8_300e_coco", checkpoints_dir="./checkpoints") # 推論処理 inference(checkpoint_file=str(checkpoint_file), model_name="yolox_l_8x8_300e_coco", device="cpu", input_image_file="./input.jpg", output_image_file="./output.jpg") # 結果をS3にupload output_object_key = output_prefix + input_object_key[len(input_prefix):] print(f"{output_object_key=}") upload_file("./output.jpg", bucket_name, output_object_key) if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument('--input-bucket-name', required=True, type=str) parser.add_argument('--input-object-key', required=True, type=str) args = parser.parse_args() print(f"{args=}") main(**args.__dict__)
Lambdaの時のhandlerと似ていますが、以下の流れで処理をします。
- 引数としてバケット名とオブジェクトキーが与えられます
- S3から対象のオブジェクトを
./
にダウンロード - モデルファイルをS3にあらかじめ置いているので
./checkpoints
にダウンロード ./checkpoints
からモデルファイルを探索- 推論処理
- 推論で出力された画像をS3にアップロード
S3関連の処理やMMDetectionに関する処理は、それぞれのハンドラを呼び出して処理をします。
異なる点は、入力に関する情報が異なることと、/tmp
以外のフォルダを使用できることです。
特に入力については以下の流れでコンテナ内に渡ってきています。
- EventBridgeの入力トランスフォーマでバケット名とオブジェクトキーにパース
- パースされた状態でTargetにあるパラメータで渡ってくる
- ジョブ定義に書かれたdockerのコマンドに上記のパラメータがプレースホルダとして準備
最後のdockerのコマンドは後で再掲しますが以下のようになっています。
command = [ "python", "run.py", "--input-bucket-name", "Ref::input_bucket_name", "--input-object-key", "Ref::input_object_key" ]
このRef::
の部分がTargetのパラメータの値に置き換えられます。
tfファイル
tfファイルは以下のようなフォルダ構成となっています。
├─environments │ └─dev │ locals.tf │ main.tf │ variables.tf │ └─modules ├─batch │ main.tf │ outputs.tf │ variables.tf │ ├─ecr │ main.tf │ variables.tf │ ├─event_bridge │ main.tf │ variables.tf │ ├─iam │ main.tf │ outputs.tf │ variables.tf │ ├─s3 │ main.tf │ variables.tf │ └─vpc main.tf outputs.tf variables.tf
前回と異なる点の要点は以下です。
- batchの追加
- 本記事のメイン部分です
- event_bridgeの追加
- S3のイベントをBatchで処理するために必要となります
- vpcの追加
- Batch(Fargate)を実行するためにVPC環境が必要なため今回作成しています
- 今回作成するのはPublicサブネットとなります
- iamの修正
- Lambdaのときはその実行ロールのみでしたが多くのロールが必要になります
- Batchについては、サービスロール、ジョブ実行ロール、ジョブロールが必要です
- EventBridgeについても実行ロールが必要です
- s3の修正
- イベント通知が異なるため少し修正が必要です
また、全体構成としてvariables.tf
を各フォルダに配置するなどの修正もしています。
environments/dev/
起点となるのはこちらのフォルダにあるtfファイルです。
以下のようになっています。
variables.tf
variable "project_prefix" {}
locals.tf
data "aws_caller_identity" "current" {} data "aws_region" "current" {} locals { account_id = data.aws_caller_identity.current.account_id region = data.aws_region.current.name bucket_name = "${var.project_prefix}-${local.account_id}" object_input_prefix = "input/" object_output_prefix = "output/" ecr_repository_name = var.project_prefix ecr_image_uri = "${local.account_id}.dkr.ecr.${local.region}.amazonaws.com/${local.ecr_repository_name}" environments = [ { name = "BUCKET_NAME" value = "${local.bucket_name}" }, { name = "OBJECT_INPUT_PREFIX" value = "${local.object_input_prefix}" }, { name = "OBJECT_OUTPUT_PREFIX" value = "${local.object_output_prefix}" } ] }
main.tf
provider "aws" { default_tags { tags = { project_prefix = var.project_prefix } } } module vpc { source = "../../modules/vpc" project_prefix = var.project_prefix } module ecr { source = "../../modules/ecr" project_prefix = var.project_prefix ecr_repository_name = local.ecr_repository_name } module iam { source="../../modules/iam" project_prefix = var.project_prefix account_id = local.account_id } module batch { source = "../../modules/batch" project_prefix = var.project_prefix image_uri = local.ecr_image_uri subnet_id = module.vpc.subnet_id security_group_id = module.vpc.security_group_id job_role_arn = module.iam.job_role_arn job_execution_role_arn = module.iam.job_execution_role_arn service_role_arn = module.iam.batch_service_role_arn environments = local.environments } module event_bridge { source = "../../modules/event_bridge" project_prefix = var.project_prefix execution_role_arn = module.iam.events_execution_role_arn job_queue_arn = module.batch.job_queue_arn job_definition_arn = module.batch.job_definition_arn bucket_name = local.bucket_name object_input_prefix = local.object_input_prefix } module s3 { source="../../modules/s3" project_prefix = var.project_prefix bucket_name=local.bucket_name object_input_prefix=local.object_input_prefix object_output_prefix=local.object_output_prefix }
定数の定義とモジュールの呼び出しを行っています。
また実行時にproject_prefix
を変数として与えられるようにしています。
modules/batch/
batchについては以下のようになっています。
(ここ以降のmodulesのvariables.tf
とoutputs.tf
の説明については単なる入出力のため割愛します。)
main.tf
// ジョブ定義 resource "aws_batch_job_definition" "main" { name = "${var.project_prefix}-job-definition" type = "container" platform_capabilities = [ "FARGATE", ] container_properties = jsonencode({ command = [ "python", "run.py", "--input-bucket-name", "Ref::input_bucket_name", "--input-object-key", "Ref::input_object_key" ] image = "${var.image_uri}:latest" jobRoleArn = "${var.job_role_arn}" fargatePlatformConfiguration = { platformVersion = "LATEST" } networkConfiguration = { assignPublicIp = "ENABLED" } resourceRequirements = [ { type = "VCPU" value = "1" }, { type = "MEMORY" value = "2048" } ] executionRoleArn = "${var.job_execution_role_arn}" environment = "${var.environments}" }) } // コンピューティング環境 resource "aws_batch_compute_environment" "fargate" { compute_environment_name = "${var.project_prefix}-compute-environment" compute_resources { max_vcpus = 16 security_group_ids = [ var.security_group_id ] subnets = [ var.subnet_id ] type = "FARGATE" } type = "MANAGED" service_role = var.service_role_arn } # ジョブキュー resource "aws_batch_job_queue" "job_queue" { name = "${var.project_prefix}-job-queue" state = "ENABLED" priority = 0 compute_environments = [aws_batch_compute_environment.fargate.arn] }
ジョブ定義とコンピューティング環境、ジョブキューの定義が必要です。
特にジョブ定義について定義に足りない部分があると、EventBridge側でFailedInvocations
となりデバッグが困難な状況に陥るため注意が必要です。
(私の場合はplatform_capabilities
やfargatePlatformConfiguration
が抜けていたためFailedInvocations
からなかなか進めませんでした)
その他ハマった点としては、FargateをPublicサブネットで動かすは以下の記述がないとECRからイメージをpullする際にエラーとなり、こちらもネットワーク設定側を見直してしまったりでハマりましたので記載しておきます。
networkConfiguration = { assignPublicIp = "ENABLED" }
この記述をTerraformのドキュメントから見つけられず解決に時間がかかりました。
CloudFormationの方には記載があるので、壁にぶち当たった場合はそちらも見てみることをオススメします。
最後にロールについてです。Batchに関連するロールは3つあります。
- ジョブ定義
- ジョブロール
- ジョブ実行ロール
- コンピューティング環境
- サービスロール
ジョブロールはECSで言うところのタスクロール、ジョブ実行ロールはECSで言うところのタスク実行ロールです。
開発者がS3などに指定のポリシー付与を行うためのロールはジョブロールになります。
ジョブ実行ロールはコンテナエージェントのためのロールで、AWS管理のポリシーが以下にあります。(今回はこちらをそのままアタッチしています)
arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
またサービスロールはAWS Batchの使用を開始する際にアカウント毎に以下のARNに作成される、サービスリンクロールを指定することができます。
arn:aws:iam::{アカウントID}:role/aws-service-role/batch.amazonaws.com/AWSServiceRoleForBatch
こちらは以下のポリシーがアタッチされています。
arn:aws:iam::aws:policy/aws-service-role/BatchServiceRolePolicy
このロールまたはポリシーがアタッチされたロールはTerraformなどで自由に作成することはできませんので注意が必要です。
今回はサービスロールもTerraformで作成しましたが、その場合は以下のように別なポリシーを使用して作成することが可能です。
arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole
ただしマネジメントコンソールでは以下のようにサービスリンクロールを使う方を推奨する旨が書いてあります。
AWS Batch では、Batch のサービスにリンクされたロールの使用を推奨しますが、AWSBatchServiceRole Identity and Access Management (IAM) ロールを使用して、お客様の下位互換性を維持することができます。これらのロールを使用して作成されたリソースは、今後サービスにリンクされたロールに変更することはできません。
modules/ecr/
ecrは変更がないため割愛します。
modules/event_bridge/
event_bridgeは以下のようになります。
# ルール resource "aws_cloudwatch_event_rule" "rule" { name = "${var.project_prefix}-event-rule" event_pattern = jsonencode({ "source" : ["aws.s3"], "detail-type" : ["Object Created"], "detail" : { "bucket" : { "name" : ["${var.bucket_name}"] }, "object" : { "key" : [{ "prefix" : "${var.object_input_prefix}" }] } } }) } # ターゲット resource "aws_cloudwatch_event_target" "target" { rule = aws_cloudwatch_event_rule.rule.name arn = var.job_queue_arn # aws_batch_job_queue.job_queue.arn # ジョブキューのARN batch_target { job_definition = var.job_definition_arn job_name = "${var.project_prefix}-job" } role_arn = var.execution_role_arn input_transformer { input_paths = { "input_bucket_name" : "$.detail.bucket.name", "input_object_key" : "$.detail.object.key" } input_template = <<-TEMPLATE {"Parameters": {"input_bucket_name":"<input_bucket_name>", "input_object_key":"<input_object_key>"}} TEMPLATE } }
event_patternの記法についてはCloudTrailが出力するJSON構造に則っています。EventBridgeのマネジメントコンソールからサンプルのイベントを見ることでも確認が可能です。event_patternはこれらの構造に合うように記載すれば良さそうです。
またその構造に合わせて値や条件文を記載する必要があります。詳細は公式ドキュメントを参照下さい。
今回使用しているprefixなどは以下に記載されています。
例えば今回のようにバケット名とオブジェクトのprefixを条件にする場合は以下のようにします。
- イベント構造
{ "detail-type": "Object Created", "source": "aws.s3", "detail": { "bucket": { "name": "example-bucket" }, "object": { "key": "example-key", } }
- イベントパターン
{ "source" : ["aws.s3"], "detail-type" : ["Object Created"], "detail" : { "bucket" : { "name" : ["${var.bucket_name}"] }, "object" : { "key" : [{ "prefix" : "${var.object_input_prefix}" }] } } }
イベント側には四角括弧[]
でマッチするものが囲われているのが特徴的です。
またここまでルール側について述べましたが、ターゲット側としてもイベントのJSON構造を考慮した作りで入力トランスフォーマの入力値(input_paths
)にアクセスしています。以下の$.detail
などの部分が該当します。
input_transformer { input_paths = { "input_bucket_name" : "$.detail.bucket.name", "input_object_key" : "$.detail.object.key" } input_template = <<-TEMPLATE {"Parameters": {"input_bucket_name":"<input_bucket_name>", "input_object_key":"<input_object_key>"}} TEMPLATE }
またそれだけではなく、これはBatchジョブ側に渡す仕組みとしてParameters
に値を詰めることで、例えばdockerのコマンドでRef::
を使ってParameters
の値を参照して置き換えることができます。
最後にターゲット側にはターゲットへ何かしらのAPI(今回の場合はSubmitJob
)を呼び出すことで後続処理を動かしますので、ターゲット側に権限としての実行ロールが必要となります。
modules/iam/
iamはここまで説明したようにBatchで3つのロール、EventBridgeで1つのロールが必要ですので、以下のようにそれぞれを定義します。
# 信頼ポリシー data "aws_iam_policy_document" "trust_ecs_tasks" { statement { effect = "Allow" principals { type = "Service" identifiers = ["ecs-tasks.amazonaws.com"] } actions = ["sts:AssumeRole"] } } # ジョブロール resource "aws_iam_role" "job_role" { name = "${var.project_prefix}-job-role" assume_role_policy = data.aws_iam_policy_document.trust_ecs_tasks.json } # IAMポリシーデータ data "aws_iam_policy_document" "job_role" { statement { effect = "Allow" actions = [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ] resources = ["*"] } statement { effect = "Allow" actions = [ "s3:*", "s3-object-lambda:*" ] resources = [ "arn:aws:s3:::${var.project_prefix}-${var.account_id}", "arn:aws:s3:::${var.project_prefix}-${var.account_id}/*" ] } } # IAMポリシー resource "aws_iam_policy" "job_role" { name = "${var.project_prefix}-job-role-policy" policy = data.aws_iam_policy_document.job_role.json } # IAMポリシーのアタッチ resource "aws_iam_role_policy_attachment" "job_role" { role = aws_iam_role.job_role.name policy_arn = aws_iam_policy.job_role.arn } # ジョブ実行ロール resource "aws_iam_role" "job_execution_role" { name = "${var.project_prefix}-job-execution-role" assume_role_policy = data.aws_iam_policy_document.trust_ecs_tasks.json managed_policy_arns = [ "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" ] } # 信頼ポリシー data "aws_iam_policy_document" "trust_batch" { statement { effect = "Allow" principals { type = "Service" identifiers = ["batch.amazonaws.com"] } actions = ["sts:AssumeRole"] } } # サービスロール resource "aws_iam_role" "batch_service_role" { name = "${var.project_prefix}-batch-service-role" assume_role_policy = data.aws_iam_policy_document.trust_batch.json managed_policy_arns = [ "arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole" ] } # 信頼ポリシー data "aws_iam_policy_document" "trust_events" { statement { effect = "Allow" principals { type = "Service" identifiers = ["events.amazonaws.com"] } actions = ["sts:AssumeRole"] } } # 実行ロール resource "aws_iam_role" "events_execution_role" { name = "${var.project_prefix}-events-execution-role" assume_role_policy = data.aws_iam_policy_document.trust_events.json } # IAMポリシーデータ data "aws_iam_policy_document" "events_execution_role" { statement { effect = "Allow" actions = [ "batch:SubmitJob" ] resources = [ "arn:aws:batch:ap-northeast-1:${var.account_id}:job/${var.project_prefix}-job", "arn:aws:batch:ap-northeast-1:${var.account_id}:job-definition/${var.project_prefix}-job-definition:*", "arn:aws:batch:ap-northeast-1:${var.account_id}:job-queue/${var.project_prefix}-job-queue" ] } } # IAMポリシー resource "aws_iam_policy" "events_execution_role" { name = "${var.project_prefix}-events-execution-role-policy" policy = data.aws_iam_policy_document.events_execution_role.json } # IAMポリシーのアタッチ resource "aws_iam_role_policy_attachment" "events_execution_role" { role = aws_iam_role.events_execution_role.name policy_arn = aws_iam_policy.events_execution_role.arn }
ジョブロールについては実際に処理の実行に必要のあるポリシーを、ジョブ実行ロールとサービスロールはAWS管理ポリシーをそのまま使用しています。
EventBridgeの実行ロールはターゲットを呼び出すために必要なポリシーを記載しています。
また、今回記事を書くにあたってロールにポリシーをアタッチするのに様々な手法があることに気が付きました。
- 自分でポリシーを書く場合
- Data Sourceを使ってHCLで記述してResourceに読み込んでアタッチする
- inline_policyでロールの中に記載
- AWS管理ポリシーを使う場合
- managed_policy_arnsで直接ポリシーを指定
- Data SourceにAWS管理ポリシーを記載してResourceに読み込んでアタッチする(こちらは実際にはカスタムポリシーが作成されるため冗長)
本記事では、自分でポリシーを記載したジョブロールとEventBridgeの実行ロールはHCL形式で記載できる「Data Sourceを使ってHCLで記述してResourceに読み込んでアタッチする」を選択し、AWS管理ポリシーを使ったジョブ実行ロールとサービスロールは記述が短くて済む「managed_policy_arnsで直接ポリシーを指定」を選択しました。
記載方法の違いについては以下も参考となりましたので、ご参照下さい。
modules/s3/
s3のリソース構成は以下のようにしています。
resource "aws_s3_bucket" "main" { bucket = var.bucket_name force_destroy = true } resource "aws_s3_bucket_notification" "bucket_notification" { bucket = aws_s3_bucket.main.id eventbridge = true } resource "aws_s3_object" "object_input" { bucket = var.bucket_name key = var.object_input_prefix depends_on = [aws_s3_bucket.main] } resource "aws_s3_object" "object_output" { bucket = var.bucket_name key = var.object_output_prefix depends_on = [aws_s3_bucket.main] } resource "aws_s3_object" "object_model_file" { bucket = var.bucket_name key = "asset/yolox_l_8x8_300e_coco_20211126_140236-d3bd2b23.pth" source = "../../../asset/yolox_l_8x8_300e_coco_20211126_140236-d3bd2b23.pth" depends_on = [aws_s3_bucket.main] }
変更をしたのはaws_s3_bucket_notification
のみなのですが、同じリソース名なのは少し以外でした。
Lambdaの時と比較すると以下です。
resource "aws_s3_bucket_notification" "bucket_notification" { bucket = aws_s3_bucket.main.id lambda_function { lambda_function_arn = var.lamba_function_arn events = ["s3:ObjectCreated:*"] filter_prefix = "input/" filter_suffix = ".jpg" } depends_on = [aws_lambda_permission.permission] }
modules/vpc/
vpcのリソース構成は以下のようにしています。
// VPC resource "aws_vpc" "vpc" { cidr_block = "10.0.0.0/16" tags = { Name = var.project_prefix } } // インターネットゲートウェイ作成 resource "aws_internet_gateway" "igw" { vpc_id = aws_vpc.vpc.id tags = { Name = var.project_prefix } } // サブネット作成 (パブリック) resource "aws_subnet" "public_1a" { vpc_id = aws_vpc.vpc.id cidr_block = "10.0.0.0/24" availability_zone = "ap-northeast-1a" map_public_ip_on_launch = true tags = { Name = var.project_prefix } } // ルートテーブル作成とインターネットゲートウェイへのルート追加 resource "aws_route_table" "public" { vpc_id = aws_vpc.vpc.id tags = { Name = var.project_prefix } } // ルート resource "aws_route" "public" { route_table_id = aws_route_table.public.id gateway_id = aws_internet_gateway.igw.id destination_cidr_block = "0.0.0.0/0" } // サブネットにルートテーブルを関連付け resource "aws_route_table_association" "route_table_association" { subnet_id = aws_subnet.public_1a.id route_table_id = aws_route_table.public.id } // セキュリティグループ作成 resource "aws_security_group" "sg" { name = "${var.project_prefix}-sg" vpc_id = aws_vpc.vpc.id egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } }
名前の見分けが困難になるため、TagsのNameに指定されたプロジェクトプレフィックスを設定するようにしています。
インターネットゲートウェイを作成して、Publicなサブネットを構築しています。
セキュリティグループはegressをすべて許可する設定にしています。
AZ名だけはべた書きとなっていますのでご注意ください。
手順
今回はLambdaと違い、事前にECRだけ単体で構築する必要はないためシンプルになります。
Terraformでリソースを全て構築
最初にリソースをすべて作成します。
# 作業フォルダ: terraform/environments/dev/ aws-vault exec {プロファイル名} -- terraform apply -var 'project_prefix={任意のプレフィックス}' # aws-vault経由で実行
Lambdaの時と異なり、ECRレポジトリにimageがpushされていなくてもBatchのジョブ定義は作成できますので、最初にすべて作ることが可能です。
イメージのビルド
まずdocker/lambda/.env
というファイルを作成して、環境変数を入力しておきます。
PROJECT_PREFIX="{任意のプレフィックス}"
{任意のプレフィックス}は、後述のterraform apply
の際に指定したものと合致するようにしておいてください。
その後は以下でビルドができます。
# 作業フォルダ: docker/lambda/ docker compose build
ECRへコンテナイメージをpush
push_ecr.sh
というスクリプトを準備していますのでそちらを実行してください。
.\push_ecr.sh {プロファイル名} {任意のプレフィックス}
{任意のプレフィックス}は、後述のterraform apply
の際に指定したものと合致するようにしておいてください。
push_ecr.sh
の内容は以下です。
ProfileName=$1 ProjectPrefix=$2 REGION=$(aws --profile $ProfileName configure get region) ACCOUNT_ID=$(aws --profile $ProfileName sts get-caller-identity --query 'Account' --output text ) REPOSITORY_NAME=$ProjectPrefix ECR_BASE_URL="${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com" ECR_IMAGE_URI="${ECR_BASE_URL}/${REPOSITORY_NAME}" echo "ECR_BASE_URL: ${ECR_BASE_URL}" echo "ECR_IMAGE_URI: ${ECR_IMAGE_URI}" # ECRへのログイン aws --profile $ProfileName ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${ECR_BASE_URL} # tagの付け替え docker tag "${REPOSITORY_NAME}:latest" "${ECR_IMAGE_URI}:latest" # ECRへのpush docker push "${ECR_IMAGE_URI}:latest"
今回からPowershellではなくGit Bashなどからshellを使う形にしています。
以上で構築の準備が完了しました。
動作確認
AWS CLIでjpgファイルをアップロードしてみます。
前回同様にasset/demo.jpg
にサンプル画像を配置してありますので良ければお試しください。
(今回はオブジェクトキーは時刻情報が付与するようにしています)
aws s3 cp asset/demo.jpg s3://{バケット名}/input/$(date "+%Y%m%d-%H%M%S").jpg --profile {プロファイル名}
処理が終わると、output/
に結果が配置されます。
aws s3 ls s3://{バケット名}/output/ --profile {プロファイル名} # 2023-09-16 21:05:52 0 # 2023-09-17 08:54:53 78343 20230917-085109.jpg
マネジメントコンソールで確認するとジョブのログが以下のように確認できます。
ジョブの作成はすぐに行われるのですが、開始まで3分30秒と結構時間が掛かっています。
こちらは感触としてはイメージのサイズなどにも影響されて変わっているようでした。
処理結果については前回と同様ですので割愛いたします。
まとめ
いかがでしたでしょうか。
今回はFargateで機械学習のワークロードを構築する方法を見ていきました。
本記事が皆様のお役に立てば幸いです。